﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

using ProjectEuler.Linq;

namespace ProjectEuler.Sudoku
{
    public sealed class SudokuPuzzle
    {
        private readonly SudokuNode[,] _nodes;
        private readonly ReadOnlyCollection<SudokuNode> _freeNodes;

        private readonly Int32 _size;
        private readonly Int32 _regionSize;

        public SudokuPuzzle(Int32[,] buffer)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }

            if (buffer.GetLength(0) != buffer.GetLength(1))
            {
                throw new ArgumentException("Puzzle must be a square.", "buffer");
            }

            if (!buffer.GetLength(0).IsSquare())
            {
                throw new ArgumentException("Puzzle size must be a square number.", "buffer");
            }

            _size = buffer.GetLength(0);
            _regionSize = (Int32)Math.Sqrt(_size);

            _nodes = CreateNodes(buffer);
            _freeNodes = new ReadOnlyCollection<SudokuNode>(FindFreeNodes().ToArray());
        }

        public SudokuNode this[Int32 row, Int32 col]
        {
            get { return _nodes[row, col]; }
        }

        public IList<SudokuNode> FreeNodes
        {
            get { return _freeNodes; }
        }

        public Int32 Size
        {
            get { return _size; }
        }

        public override String ToString()
        {
            var digits = (Int32)Math.Ceiling(Math.Log10(_size));
            var format = new String('0', digits);
            var result = new StringBuilder();

            for (int y = 0; y < _size; y++)
            {
                for (int x = 0; x < _size; x++)
                {
                    result.AppendFormat(_nodes[y, x].Value.ToString(format));
                    result.Append(' ');

                    if ((x < (_size - 1)) && (((x + 1) % _regionSize) == 0))
                    {
                        result.Append("| ");
                    }
                }

                result.AppendLine();

                if ((y < (_size - 1)) && (((y + 1) % _regionSize) == 0))
                {
                    for (int x = 0; x < _size; x++)
                    {
                        result.Append(new String('-', digits + 1));

                        if ((x < (_size - 1)) && (((x + 1) % _regionSize) == 0))
                        {
                            result.Append("+-");
                        }
                    }

                    result.AppendLine();
                }
            }

            return result.AppendLine().ToString();
        }

        public IEnumerable<SudokuNode> Row(SudokuNode node)
        {
            return Row(node.Row);
        }

        public IEnumerable<SudokuNode> Row(Int32 y)
        {
            for (int x = 0; x < _size; x++)
            {
                yield return _nodes[y, x];
            }
        }

        public IEnumerable<SudokuNode> Column(SudokuNode node)
        {
            return Column(node.Column);
        }

        public IEnumerable<SudokuNode> Column(Int32 x)
        {
            for (int y = 0; y < _size; y++)
            {
                yield return _nodes[y, x];
            }
        }

        public IEnumerable<SudokuNode> Region(SudokuNode node)
        {
            return Region(node.Region);
        }

        public IEnumerable<SudokuNode> Region(Int32 n)
        {
            Int32 rx;
            Int32 ry = Math.DivRem(n, _regionSize, out rx);

            for (int y = 0; y < _regionSize; y++)
            {
                for (int x = 0; x < _regionSize; x++)
                {
                    yield return _nodes[(ry * _regionSize) + y, (rx * _regionSize) + x];
                }
            }
        }


        private IEnumerable<SudokuNode> FindFreeNodes()
        {
            for (int y = 0; y < _size; y++)
            {
                for (int x = 0; x < _size; x++)
                {
                    if (!_nodes[y, x].IsFixed)
                    {
                        yield return _nodes[y, x];
                    }
                }
            }
        }

        private SudokuNode[,] CreateNodes(Int32[,] buffer)
        {
            var nodes = new SudokuNode[_size, _size];
            var rs = (Double)(_regionSize);

            for (var y = 0; y < _size; y++)
            {
                for (var x = 0; x < _size; x++)
                {
                    var rn = (Int32)(rs * Math.Floor(y / rs) + (x / rs));

                    if (buffer[y, x] == 0)
                    {
                        nodes[y, x] = new SudokuNode(y, x, rn);
                    }
                    else
                    {
                        nodes[y, x] = new SudokuNode(buffer[y, x], true, y, x, rn);
                    }
                }
            }

            return nodes;
        }
    }
}
